package com.ruoyi.common.config;

import com.ruoyi.common.filter.RestTemplateInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Slf4j
@Configuration
public class RestTemplateConfiguration {

    /**
     * 官方推荐使用检查永久链接的可用性,而不推荐每次请求的时候才去检查 (milliseconds 毫秒)
     */
    private int connectionValidateAfterInactivityMs = 20 * 1000;

    /**
     * 连接池的最大连接数
     */
    private int maxTotalConnect = 50;

    /**
     * 同路由的并发数 每个路由的最大连接数,如果只调用一个地址,可以将其设置为最大连接数
     */
    private int maxConnectPerRoute = 10;

    /**
     * 客户端和服务器建立连接超时，默认15s 指客户端和服务器建立连接的超时时间,ms , 最大约21秒,因为内部tcp在进行三次握手建立连接时,默认tcp超时时间是20秒
     */
    private int connectTimeout = 20 * 1000;

    /**
     * 指客户端从服务器读取数据包的间隔超时时间,不是总读取时间,默认20s
     */
    private int readTimeout = 120 * 1000;

    /**
     * 重试次数,默认2次
     */
    private int retryTimes = 1;

    /**
     * 从连接池获取连接的超时时间,不宜过长,单位ms
     */
    private int connectionRequestTimout = 200;

    /**
     * 针对不同的地址,特别设置不同的长连接保持时间,单位s,如果是频繁而持续的请求,可以设置小一点,不建议设置过大,避免大量无用连接占用内存资源
     */
    private static Map<String, Integer> keepAliveTargetHost = new HashMap<>();

    /**
     * 长连接保持时间 单位s,不宜过长
     */
    private int keepAliveTime = 10;

    @Resource
    private RestTemplateInterceptor restTemplateLog;

    /**
     * 设置httpclient参数
     */
    @Bean
    public HttpClient httpClient(ConnectionKeepAliveStrategy connectionKeepAliveStrategy) {
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        // 使用Httpclient连接池的方式配置(推荐)，同时支持netty，okHttp以及其他http框架
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        // 最大连接数
        poolingHttpClientConnectionManager.setMaxTotal(maxTotalConnect);
        // 同路由并发数
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(maxConnectPerRoute);
        // 官方推荐使用检查永久链接的可用性,而不推荐每次请求的时候才去检查 (milliseconds 毫秒)
        poolingHttpClientConnectionManager.setValidateAfterInactivity(connectionValidateAfterInactivityMs);

        // 配置连接池
        httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);

        // 重试次数
        httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(retryTimes, true, new ArrayList<>()) {
            @Override
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                return super.retryRequest(exception, executionCount, context);
            }
        });

        // 设置长连接保持策略
        httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy);

        /**
         * 处理Invalid cookie header和Cookie rejected错误，配置Cookie策略为ignoreCookies
         * cookieSpec:即cookie策略。参数为cookiespecs的一些字段。作用： 1、如果网站header中有set-cookie字段时，采用默认方式可能会被cookie
         * reject，无法写入cookie。将此属性设置成CookieSpecs.STANDARD_STRICT可避免此情况。
         * 2、如果要想忽略cookie访问，则将此属性设置成CookieSpecs.IGNORE_COOKIES。
         */
        httpClientBuilder
                .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build());
        return httpClientBuilder.build();
    }

    /**
     * 创建HTTP客户端工厂
     */
    @Bean
    public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
        HttpComponentsClientHttpRequestFactory clientHttpRequestFactory =
                new HttpComponentsClientHttpRequestFactory(httpClient);
        // 连接超时
        clientHttpRequestFactory.setConnectTimeout(connectTimeout);
        // 数据读取超时时间，即SocketTimeout
        clientHttpRequestFactory.setReadTimeout(readTimeout);
        // 从连接池获取请求连接的超时时间，不宜过长，必须设置，比如连接不够用时，时间过长将是灾难性的
        clientHttpRequestFactory.setConnectionRequestTimeout(connectionRequestTimout);
        return clientHttpRequestFactory;
    }

    /**
     * 设置长连接保持策略
     */
    @Bean
    public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return (response, context) -> {
            // response的header中存在keep-alive字段使用timeout指定的时间
            HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && "timeout".equalsIgnoreCase(param)) {
                    try {
                        return Long.parseLong(value) * 1000;
                    } catch (NumberFormatException ignore) {
                        log.error("解析长连接过期时间异常 {}", ignore);
                    }
                }
            }
            // response没有指定keep-alive时长，使用配置的时长，如果不在配置的ip，使用默认时长
            HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
            // 如果请求目标地址,单独配置了长连接保持时间,使用该配置
            Optional<Map.Entry<String, Integer>> any = Optional.ofNullable(keepAliveTargetHost).orElseGet(HashMap::new)
                    .entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(target.getHostName())).findAny();
            // 否则使用默认长连接保持时间
            Long aLong = any.map(en -> en.getValue() * 1000L).orElse(keepAliveTime * 1000L);
            return aLong;
        };
    }

    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        // 解决401报错时，报java.net.HttpRetryException: cannot retry due to server authentication, in streaming mode
        requestFactory.setOutputStreaming(false);
        RestTemplate restTemplate = new RestTemplate(requestFactory);

        // 添加拦截器
        List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
        interceptors.add(restTemplateLog);
        restTemplate.setInterceptors(interceptors);

        // 提供对传出/传入流的缓冲,可以让响应body多次读取(如果不配置,拦截器读取了Response流,再响应数据时会返回body=null)
        restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(clientHttpRequestFactory));

        // 我们采用RestTemplate内部的MessageConverter
        // 重新设置StringHttpMessageConverter字符集为UTF-8，解决中文乱码问题
        modifyDefaultCharset(restTemplate);

        // 请求失败异常处理，如果不重写hasError方法，抛出异常，无法执行后续代码
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public boolean hasError(ClientHttpResponse response) {
                return false;
            }
        });
        return restTemplate;
    }

    private void modifyDefaultCharset(RestTemplate restTemplate) {
        List<HttpMessageConverter<?>> converterList = restTemplate.getMessageConverters();
        HttpMessageConverter<?> converterTarget = null;
        for (HttpMessageConverter<?> item : converterList) {
            if (StringHttpMessageConverter.class == item.getClass()) {
                converterTarget = item;
                break;
            }
        }
        if (null != converterTarget) {
            converterList.remove(converterTarget);
        }
        Charset defaultCharset = StandardCharsets.UTF_8;
        converterList.add(1, new StringHttpMessageConverter(defaultCharset));
    }

}


